﻿/*--------------------------------------------------------------------------
 * linq.js - LINQ for JavaScript
 * licensed under MIT License
 *------------------------------------------------------------------------*/

var Functions = {
    Identity: function (x) { return x; },
    True: function () { return true; },
    Blank: function () { }
};

var Types = {
    Boolean: typeof true,
    Number: typeof 0,
    String: typeof "",
    Object: typeof {},
    Undefined: typeof undefined,
    Function: typeof function () { }
};

var funcCache = { "": Functions.Identity };

var Utils = {
    createLambda: function (expression) {
        if (expression == null) return Functions.Identity;
        if (typeof expression === Types.String) {
            // get from cache
            let f = funcCache[expression];
            if (f != null) {
                return f;
            }

            if (expression.indexOf("=>") === -1) {
                const regexp = new RegExp("[$]+", "g");

                let maxLength = 0;
                let match;
                while ((match = regexp.exec(expression)) != null) {
                    if (match[0].length > maxLength) {
                        maxLength = match[0].length;
                    }
                }

                const argArray = [];
                for (let i = 1; i <= maxLength; i++) {
                    let dollar = "";
                    for (let j = 0; j < i; j++) {
                        dollar += "$";
                    }
                    argArray.push(dollar);
                }

                const args = argArray.join(",");

                f = new Function(args, "return " + expression);
                funcCache[expression] = f;
                return f;
            }
            else {
                const expr = expression.match(/^[(\s]*([^()]*?)[)\s]*=>(.*)/);
                f = new Function(expr[1], (expr[2].match(/\breturn\b/) ? expr[2] : "return " + expr[2]));
                funcCache[expression] = f;
                return f;
            }
        }
        return expression;
    },

    defineProperty: function (target, methodName, value) {
        Object.defineProperty(target, methodName, {
            enumerable: false,
            configurable: true,
            writable: true,
            value: value
        })
    },

    compare: function (a, b) {
        return (a === b) ? 0 : (a > b) ? 1 : -1;
    },

    dispose: function (obj) {
        if (obj != null) obj.dispose();
    },

    hasNativeIteratorSupport: function () {
        return typeof Symbol !== 'undefined' && typeof Symbol.iterator !== 'undefined';
    }
};

var State = { Before: 0, Running: 1, After: 2 };

var IEnumerator = function (initialize, tryGetNext, dispose) {
    var yielder = new Yielder();
    var state = State.Before;

    this.current = yielder.current;

    this.moveNext = function () {
        try {
            switch (state) {
                case State.Before:
                    state = State.Running;
                    initialize();
                // fall through

                case State.Running:
                    if (tryGetNext.apply(yielder)) {
                        return true;
                    }
                    else {
                        this.dispose();
                        return false;
                    }
                // fall through

                case State.After:
                    return false;
            }
        }
        catch (e) {
            this.dispose();
            throw e;
        }
    };

    this.dispose = function () {
        if (state != State.Running) return;

        try {
            dispose();
        }
        finally {
            state = State.After;
        }
    };
};

// tryGetNext yielder
var Yielder = function () {
    var current = null;
    this.current = function () { return current; };
    this.yieldReturn = function (value) {
        current = value;
        return true;
    };
    this.yieldBreak = function () {
        return false;
    };
};

// Enumerable constuctor
var Enumerable = function (getEnumerator) {
    this.getEnumerator = getEnumerator;
};

///////////////////
// Utility Methods

Enumerable.Utils = {};

Enumerable.Utils.createLambda = function (expression) {
    return Utils.createLambda(expression);
};

Enumerable.Utils.createEnumerable = function (getEnumerator) {
    return new Enumerable(getEnumerator);
};

Enumerable.Utils.createEnumerator = function (initialize, tryGetNext, dispose) {
    return new IEnumerator(initialize, tryGetNext, dispose);
};

Enumerable.Utils.extendTo = function (type) {
    var typeProto = type.prototype;
    var enumerableProto;

    if (type === Array) {
        enumerableProto = ArrayEnumerable.prototype;
        Utils.defineProperty(typeProto, "getSource", function () {
            return this;
        });
    }
    else {
        enumerableProto = Enumerable.prototype;
        Utils.defineProperty(typeProto, "getEnumerator", function () {
            return Enumerable.from(this).getEnumerator();
        });
    }

    for (let methodName in enumerableProto) {
        const func = enumerableProto[methodName];

        // already extended
        if (typeProto[methodName] == func) continue;

        // already defined(example Array#reverse/join/forEach...)
        if (typeProto[methodName] != null) {
            methodName = methodName + "ByLinq";
            if (typeProto[methodName] == func) continue; // recheck
        }

        if (func instanceof Function) {
            Utils.defineProperty(typeProto, methodName, func);
        }
    }
};

Enumerable.Utils.recallFrom = function (type) {
    var typeProto = type.prototype;
    var enumerableProto;

    if (type === Array) {
        enumerableProto = ArrayEnumerable.prototype;
        delete typeProto.getSource;
    }
    else {
        enumerableProto = Enumerable.prototype;
        delete typeProto.getEnumerator;
    }

    for (const methodName in enumerableProto) {
        const func = enumerableProto[methodName];

        if (typeProto[methodName + 'ByLinq']) {
            delete typeProto[methodName + 'ByLinq'];
        }
        else if (typeProto[methodName] == func && func instanceof Function) {
            delete typeProto[methodName];
        }
    }
};

//////////////
// Generators

Enumerable.choice = function () {
    var args = arguments;

    return new Enumerable(function () {
        return new IEnumerator(
            function () {
                args = (args[0] instanceof Array) ? args[0]
                    : (args[0].getEnumerator != null) ? args[0].toArray()
                        : args;
            },
            function () {
                return this.yieldReturn(args[Math.floor(Math.random() * args.length)]);
            },
            Functions.Blank);
    });
};

Enumerable.cycle = function () {
    var args = arguments;

    return new Enumerable(function () {
        var index = 0;
        return new IEnumerator(
            function () {
                args = (args[0] instanceof Array) ? args[0]
                    : (args[0].getEnumerator != null) ? args[0].toArray()
                        : args;
            },
            function () {
                if (index >= args.length) index = 0;
                return this.yieldReturn(args[index++]);
            },
            Functions.Blank);
    });
};

Enumerable.empty = function () {
    return new Enumerable(function () {
        return new IEnumerator(
            Functions.Blank,
            function () { return false; },
            Functions.Blank);
    });
};

Enumerable.from = function (obj) {
    if (obj == null) {
        return Enumerable.empty();
    }
    if (obj instanceof Enumerable) {
        return obj;
    }
    if (typeof obj == Types.Number || typeof obj == Types.Boolean) {
        return Enumerable.repeat(obj, 1);
    }
    if (typeof obj == Types.String) {
        return new Enumerable(function () {
            var index = 0;
            return new IEnumerator(
                Functions.Blank,
                function () {
                    return (index < obj.length) ? this.yieldReturn(obj.charAt(index++)) : false;
                },
                Functions.Blank);
        });
    }
    if (typeof obj == Types.Function && Object.keys(obj).length == 0) {
        return new Enumerable(function () {
            var orig;

            return new IEnumerator(
                function () {
                    orig = obj()[Symbol.iterator]();
                },
                function () {
                    var next = orig.next();
                    return (next.done ? false : (this.yieldReturn(next.value)));
                },
                Functions.Blank);
        });
    }

    if (typeof obj != Types.Function) {
        // array or array-like object
        if (typeof obj.length == Types.Number) {
            return new ArrayEnumerable(obj);
        }

        // iterable object
        if (typeof Symbol !== 'undefined' && typeof obj[Symbol.iterator] !== 'undefined') {
            let iterator;
            return new Enumerable(function () {
                return new IEnumerator(
                    function () { iterator = obj[Symbol.iterator]()},
                    function () {
                        var next = iterator.next();
                        return (next.done ? false : (this.yieldReturn(next.value)));
                    },
                    Functions.Blank);
            });
        }

        // object conforming to the iterator protocol
        if (typeof obj.next == Types.Function) {
            return new Enumerable(function () {
                return new IEnumerator(
                    Functions.Blank,
                    function () {
                        var next = obj.next();
                        return (next.done ? false : (this.yieldReturn(next.value)));
                    },
                    Functions.Blank);
            });
        }
    }

    // case function/object: create keyValuePair[]
    return new Enumerable(function () {
        var array = [];
        var index = 0;

        return new IEnumerator(
            function () {
                for (const key in obj) {
                    const value = obj[key];
                    if (!(value instanceof Function) && Object.prototype.hasOwnProperty.call(obj, key)) {
                        array.push({ key: key, value: value });
                    }
                }
            },
            function () {
                return (index < array.length)
                    ? this.yieldReturn(array[index++])
                    : false;
            },
            Functions.Blank);
    });
},

    Enumerable.make = function (element) {
        return Enumerable.repeat(element, 1);
    };

// Overload:function(input, pattern)
// Overload:function(input, pattern, flags)
Enumerable.matches = function (input, pattern, flags) {
    if (flags == null) flags = "";

    if (pattern instanceof RegExp) {
        flags += (pattern.ignoreCase) ? "i" : "";
        flags += (pattern.multiline) ? "m" : "";
        pattern = pattern.source;
    }
    if (flags.indexOf("g") === -1) flags += "g";

    return new Enumerable(function () {
        var regex;
        return new IEnumerator(
            function () { regex = new RegExp(pattern, flags); },
            function () {
                var match = regex.exec(input);
                return (match) ? this.yieldReturn(match) : false;
            },
            Functions.Blank);
    });
};

// Overload:function(start, count)
// Overload:function(start, count, step)
Enumerable.range = function (start, count, step) {
    if (step == null) step = 1;

    return new Enumerable(function () {
        var value;
        var index = 0;

        return new IEnumerator(
            function () { value = start - step; },
            function () {
                return (index++ < count)
                    ? this.yieldReturn(value += step)
                    : this.yieldBreak();
            },
            Functions.Blank);
    });
};

// Overload:function(start, count)
// Overload:function(start, count, step)
Enumerable.rangeDown = function (start, count, step) {
    if (step == null) step = 1;

    return new Enumerable(function () {
        var value;
        var index = 0;

        return new IEnumerator(
            function () { value = start + step; },
            function () {
                return (index++ < count)
                    ? this.yieldReturn(value -= step)
                    : this.yieldBreak();
            },
            Functions.Blank);
    });
};

// Overload:function(start, to)
// Overload:function(start, to, step)
Enumerable.rangeTo = function (start, to, step) {
    if (step == null) step = 1;

    if (start < to) {
        return new Enumerable(function () {
            var value;

            return new IEnumerator(
                function () { value = start - step; },
                function () {
                    var next = value += step;
                    return (next <= to)
                        ? this.yieldReturn(next)
                        : this.yieldBreak();
                },
                Functions.Blank);
        });
    }
    else {
        return new Enumerable(function () {
            var value;

            return new IEnumerator(
                function () { value = start + step; },
                function () {
                    var next = value -= step;
                    return (next >= to)
                        ? this.yieldReturn(next)
                        : this.yieldBreak();
                },
                Functions.Blank);
        });
    }
};

// Overload:function(element)
// Overload:function(element, count)
Enumerable.repeat = function (element, count) {
    if (count != null)
        return Enumerable.repeat(element).take(count);

    return new Enumerable(function () {
        return new IEnumerator(
            Functions.Blank,
            function () { return this.yieldReturn(element); },
            Functions.Blank);
    });
};

Enumerable.repeatWithFinalize = function (initializer, finalizer) {
    initializer = Utils.createLambda(initializer);
    finalizer = Utils.createLambda(finalizer);

    return new Enumerable(function () {
        var element;
        return new IEnumerator(
            function () { element = initializer(); },
            function () { return this.yieldReturn(element); },
            function () {
                if (element != null) {
                    finalizer(element);
                    element = null;
                }
            });
    });
};

// Overload:function(func)
// Overload:function(func, count)
Enumerable.generate = function (func, count) {
    if (count != null)
        return Enumerable.generate(func).take(count);

    func = Utils.createLambda(func);

    return new Enumerable(function () {
        return new IEnumerator(
            Functions.Blank,
            function () { return this.yieldReturn(func()); },
            Functions.Blank);
    });
};

// Overload:function()
// Overload:function(start)
// Overload:function(start, step)
Enumerable.toInfinity = function (start, step) {
    if (start == null) start = 0;
    if (step == null) step = 1;

    return new Enumerable(function () {
        var value;
        return new IEnumerator(
            function () { value = start - step; },
            function () { return this.yieldReturn(value += step); },
            Functions.Blank);
    });
};

// Overload:function()
// Overload:function(start)
// Overload:function(start, step)
Enumerable.toNegativeInfinity = function (start, step) {
    if (start == null) start = 0;
    if (step == null) step = 1;

    return new Enumerable(function () {
        var value;
        return new IEnumerator(
            function () { value = start + step; },
            function () { return this.yieldReturn(value -= step); },
            Functions.Blank);
    });
};

Enumerable.unfold = function (seed, func) {
    func = Utils.createLambda(func);

    return new Enumerable(function () {
        var isFirst = true;
        var value;
        return new IEnumerator(
            Functions.Blank,
            function () {
                if (isFirst) {
                    isFirst = false;
                    value = seed;
                    return this.yieldReturn(value);
                }
                value = func(value);
                return this.yieldReturn(value);
            },
            Functions.Blank);
    });
};

Enumerable.defer = function (enumerableFactory) {
    return new Enumerable(function () {
        var enumerator;

        return new IEnumerator(
            function () { enumerator = Enumerable.from(enumerableFactory()).getEnumerator(); },
            function () {
                return (enumerator.moveNext())
                    ? this.yieldReturn(enumerator.current())
                    : this.yieldBreak();
            },
            function () {
                Utils.dispose(enumerator);
            });
    });
};

/////////////////////
// Extension Methods

////////////////////////////////////
// Projection and Filtering Methods

// Overload:function(func)
// Overload:function(func, resultSelector<element>)
// Overload:function(func, resultSelector<element, nestLevel>)
Enumerable.prototype.traverseBreadthFirst = function (func, resultSelector) {
    var source = this;
    func = Utils.createLambda(func);
    resultSelector = Utils.createLambda(resultSelector);

    return new Enumerable(function () {
        var enumerator;
        var nestLevel = 0;
        var buffer = [];

        return new IEnumerator(
            function () { enumerator = source.getEnumerator(); },
            function () {
                while (true) {
                    if (enumerator.moveNext()) {
                        buffer.push(enumerator.current());
                        return this.yieldReturn(resultSelector(enumerator.current(), nestLevel));
                    }

                    const next = Enumerable.from(buffer).selectMany(function (x) { return func(x); });
                    if (!next.any()) {
                        return false;
                    }
                    else {
                        nestLevel++;
                        buffer = [];
                        Utils.dispose(enumerator);
                        enumerator = next.getEnumerator();
                    }
                }
            },
            function () { Utils.dispose(enumerator); });
    });
};

// Overload:function(func)
// Overload:function(func, resultSelector<element>)
// Overload:function(func, resultSelector<element, nestLevel>)
Enumerable.prototype.traverseDepthFirst = function (func, resultSelector) {
    var source = this;
    func = Utils.createLambda(func);
    resultSelector = Utils.createLambda(resultSelector);

    return new Enumerable(function () {
        var enumeratorStack = [];
        var enumerator;

        return new IEnumerator(
            function () { enumerator = source.getEnumerator(); },
            function () {
                while (true) {
                    if (enumerator.moveNext()) {
                        const value = resultSelector(enumerator.current(), enumeratorStack.length);
                        enumeratorStack.push(enumerator);
                        enumerator = Enumerable.from(func(enumerator.current())).getEnumerator();
                        return this.yieldReturn(value);
                    }

                    if (enumeratorStack.length <= 0) return false;
                    Utils.dispose(enumerator);
                    enumerator = enumeratorStack.pop();
                }
            },
            function () {
                try {
                    Utils.dispose(enumerator);
                }
                finally {
                    Enumerable.from(enumeratorStack).forEach(function (s) { s.dispose(); });
                }
            });
    });
};

Enumerable.prototype.flatten = function () {
    var source = this;

    return new Enumerable(function () {
        var enumerator;
        var middleEnumerator = null;

        return new IEnumerator(
            function () { enumerator = source.getEnumerator(); },
            function () {
                while (true) {
                    if (middleEnumerator != null) {
                        if (middleEnumerator.moveNext()) {
                            return this.yieldReturn(middleEnumerator.current());
                        }
                        else {
                            middleEnumerator = null;
                        }
                    }

                    if (enumerator.moveNext()) {
                        if (enumerator.current() instanceof Array) {
                            Utils.dispose(middleEnumerator);
                            middleEnumerator = Enumerable.from(enumerator.current())
                                .selectMany(Functions.Identity)
                                .flatten()
                                .getEnumerator();
                            continue;
                        }
                        else {
                            return this.yieldReturn(enumerator.current());
                        }
                    }

                    return false;
                }
            },
            function () {
                try {
                    Utils.dispose(enumerator);
                }
                finally {
                    Utils.dispose(middleEnumerator);
                }
            });
    });
};

Enumerable.prototype.pairwise = function (selector) {
    var source = this;
    selector = Utils.createLambda(selector);

    return new Enumerable(function () {
        var enumerator;

        return new IEnumerator(
            function () {
                enumerator = source.getEnumerator();
                enumerator.moveNext();
            },
            function () {
                var prev = enumerator.current();
                return (enumerator.moveNext())
                    ? this.yieldReturn(selector(prev, enumerator.current()))
                    : false;
            },
            function () { Utils.dispose(enumerator); });
    });
};

// Overload:function(func)
// Overload:function(seed,func<value,element>)
Enumerable.prototype.scan = function (seed, func) {
    var isUseSeed;
    if (func == null) {
        func = Utils.createLambda(seed);
        isUseSeed = false;
    } else {
        func = Utils.createLambda(func);
        isUseSeed = true;
    }
    var source = this;

    return new Enumerable(function () {
        var enumerator;
        var value;
        var isFirst = true;

        return new IEnumerator(
            function () { enumerator = source.getEnumerator(); },
            function () {
                if (isFirst) {
                    isFirst = false;
                    if (!isUseSeed) {
                        if (enumerator.moveNext()) {
                            return this.yieldReturn(value = enumerator.current());
                        }
                    }
                    else {
                        return this.yieldReturn(value = seed);
                    }
                }

                return (enumerator.moveNext())
                    ? this.yieldReturn(value = func(value, enumerator.current()))
                    : false;
            },
            function () { Utils.dispose(enumerator); });
    });
};

// Overload:function(selector<element>)
// Overload:function(selector<element,index>)
Enumerable.prototype.select = function (selector) {
    selector = Utils.createLambda(selector);

    if (selector.length <= 1) {
        return new WhereSelectEnumerable(this, null, selector);
    }
    else {
        var source = this;

        return new Enumerable(function () {
            var enumerator;
            var index = 0;

            return new IEnumerator(
                function () { enumerator = source.getEnumerator(); },
                function () {
                    return (enumerator.moveNext())
                        ? this.yieldReturn(selector(enumerator.current(), index++))
                        : false;
                },
                function () { Utils.dispose(enumerator); });
        });
    }
};

// Overload:function(collectionSelector<element>)
// Overload:function(collectionSelector<element,index>)
// Overload:function(collectionSelector<element>,resultSelector)
// Overload:function(collectionSelector<element,index>,resultSelector)
Enumerable.prototype.selectMany = function (collectionSelector, resultSelector) {
    var source = this;
    collectionSelector = Utils.createLambda(collectionSelector);
    if (resultSelector == null) resultSelector = function (a, b) { return b; };
    resultSelector = Utils.createLambda(resultSelector);

    return new Enumerable(function () {
        var enumerator;
        var middleEnumerator = undefined;
        var index = 0;

        return new IEnumerator(
            function () { enumerator = source.getEnumerator(); },
            function () {
                if (middleEnumerator === undefined) {
                    if (!enumerator.moveNext()) return false;
                }
                do {
                    if (middleEnumerator == null) {
                        const middleSeq = collectionSelector(enumerator.current(), index++);
                        middleEnumerator = Enumerable.from(middleSeq).getEnumerator();
                    }
                    if (middleEnumerator.moveNext()) {
                        return this.yieldReturn(resultSelector(enumerator.current(), middleEnumerator.current()));
                    }
                    Utils.dispose(middleEnumerator);
                    middleEnumerator = null;
                } while (enumerator.moveNext());
                return false;
            },
            function () {
                try {
                    Utils.dispose(enumerator);
                }
                finally {
                    Utils.dispose(middleEnumerator);
                }
            });
    });
};

// Overload:function(predicate<element>)
// Overload:function(predicate<element,index>)
Enumerable.prototype.where = function (predicate) {
    predicate = Utils.createLambda(predicate);

    if (predicate.length <= 1) {
        return new WhereEnumerable(this, predicate);
    }
    else {
        var source = this;

        return new Enumerable(function () {
            var enumerator;
            var index = 0;

            return new IEnumerator(
                function () { enumerator = source.getEnumerator(); },
                function () {
                    while (enumerator.moveNext()) {
                        if (predicate(enumerator.current(), index++)) {
                            return this.yieldReturn(enumerator.current());
                        }
                    }
                    return false;
                },
                function () { Utils.dispose(enumerator); });
        });
    }
};


// Overload:function(selector<element>)
// Overload:function(selector<element,index>)
Enumerable.prototype.choose = function (selector) {
    selector = Utils.createLambda(selector);
    var source = this;

    return new Enumerable(function () {
        var enumerator;
        var index = 0;

        return new IEnumerator(
            function () { enumerator = source.getEnumerator(); },
            function () {
                while (enumerator.moveNext()) {
                    const result = selector(enumerator.current(), index++);
                    if (result != null) {
                        return this.yieldReturn(result);
                    }
                }
                return this.yieldBreak();
            },
            function () { Utils.dispose(enumerator); });
    });
};

Enumerable.prototype.ofType = function (type) {
    var typeName;
    switch (type) {
        case Number:
            typeName = Types.Number;
            break;
        case String:
            typeName = Types.String;
            break;
        case Boolean:
            typeName = Types.Boolean;
            break;
        case Function:
            typeName = Types.Function;
            break;
        default:
            typeName = null;
            break;
    }
    return (typeName === null)
        ? this.where(function (x) { return x instanceof type; })
        : this.where(function (x) { return typeof x === typeName; });
};

// mutiple arguments, last one is selector, others are enumerable
Enumerable.prototype.zip = function () {
    var args = arguments;
    var selector = Utils.createLambda(arguments[arguments.length - 1]);

    var source = this;
    // optimized case:argument is 2
    if (arguments.length == 2) {
        const second = arguments[0];

        return new Enumerable(function () {
            var firstEnumerator;
            var secondEnumerator;
            var index = 0;

            return new IEnumerator(
                function () {
                    firstEnumerator = source.getEnumerator();
                    secondEnumerator = Enumerable.from(second).getEnumerator();
                },
                function () {
                    if (firstEnumerator.moveNext() && secondEnumerator.moveNext()) {
                        return this.yieldReturn(selector(firstEnumerator.current(), secondEnumerator.current(), index++));
                    }
                    return false;
                },
                function () {
                    try {
                        Utils.dispose(firstEnumerator);
                    } finally {
                        Utils.dispose(secondEnumerator);
                    }
                });
        });
    }
    else {
        return new Enumerable(function () {
            var enumerators;
            var index = 0;

            return new IEnumerator(
                function () {
                    var array = Enumerable.make(source)
                        .concat(Enumerable.from(args).takeExceptLast().select(Enumerable.from))
                        .select(function (x) { return x.getEnumerator() })
                        .toArray();
                    enumerators = Enumerable.from(array);
                },
                function () {
                    if (enumerators.all(function (x) { return x.moveNext() })) {
                        const array = enumerators
                            .select(function (x) { return x.current() })
                            .toArray();
                        array.push(index++);
                        return this.yieldReturn(selector.apply(null, array));
                    }
                    else {
                        return this.yieldBreak();
                    }
                },
                function () {
                    Enumerable.from(enumerators).forEach(Utils.dispose);
                });
        });
    }
};

// mutiple arguments
Enumerable.prototype.merge = function () {
    var args = arguments;
    var source = this;

    return new Enumerable(function () {
        var enumerators;
        var index = -1;

        return new IEnumerator(
            function () {
                enumerators = Enumerable.make(source)
                    .concat(Enumerable.from(args).select(Enumerable.from))
                    .select(function (x) { return x.getEnumerator() })
                    .toArray();
            },
            function () {
                while (enumerators.length > 0) {
                    index = (index >= enumerators.length - 1) ? 0 : index + 1;
                    const enumerator = enumerators[index];

                    if (enumerator.moveNext()) {
                        return this.yieldReturn(enumerator.current());
                    }
                    else {
                        enumerator.dispose();
                        enumerators.splice(index--, 1);
                    }
                }
                return this.yieldBreak();
            },
            function () {
                Enumerable.from(enumerators).forEach(Utils.dispose);
            });
    });
};

////////////////
// Join Methods

// Overload:function (inner, outerKeySelector, innerKeySelector, resultSelector)
// Overload:function (inner, outerKeySelector, innerKeySelector, resultSelector, compareSelector)
Enumerable.prototype.join = function (inner, outerKeySelector, innerKeySelector, resultSelector, compareSelector) {
    outerKeySelector = Utils.createLambda(outerKeySelector);
    innerKeySelector = Utils.createLambda(innerKeySelector);
    resultSelector = Utils.createLambda(resultSelector);
    compareSelector = Utils.createLambda(compareSelector);
    var source = this;

    return new Enumerable(function () {
        var outerEnumerator;
        var lookup;
        var innerElements = null;
        var innerCount = 0;

        return new IEnumerator(
            function () {
                outerEnumerator = source.getEnumerator();
                lookup = Enumerable.from(inner).toLookup(innerKeySelector, Functions.Identity, compareSelector);
            },
            function () {
                while (true) {
                    if (innerElements != null) {
                        let innerElement = innerElements[innerCount++];
                        if (innerElement !== undefined) {
                            return this.yieldReturn(resultSelector(outerEnumerator.current(), innerElement));
                        }

                        innerElement = null;
                        innerCount = 0;
                    }

                    if (outerEnumerator.moveNext()) {
                        const key = outerKeySelector(outerEnumerator.current());
                        innerElements = lookup.get(key).toArray();
                    } else {
                        return false;
                    }
                }
            },
            function () { Utils.dispose(outerEnumerator); });
    });
};

// Overload:function (inner, outerKeySelector, innerKeySelector, resultSelector)
// Overload:function (inner, outerKeySelector, innerKeySelector, resultSelector, compareSelector)
Enumerable.prototype.leftJoin = function (inner, outerKeySelector, innerKeySelector, resultSelector, compareSelector) {
    outerKeySelector = Utils.createLambda(outerKeySelector);
    innerKeySelector = Utils.createLambda(innerKeySelector);
    resultSelector = Utils.createLambda(resultSelector);
    compareSelector = Utils.createLambda(compareSelector);
    var source = this;

    return new Enumerable(function () {
        var outerEnumerator;
        var lookup;
        var innerElements = null;
        var innerCount = 0;

        return new IEnumerator(
            function () {
                outerEnumerator = source.getEnumerator();
                lookup = Enumerable.from(inner).toLookup(innerKeySelector, Functions.Identity, compareSelector);
            },
            function () {
                while (true) {
                    if (innerElements != null) {
                        let innerElement = innerElements[innerCount++];
                        if (innerElement !== undefined) {
                            return this.yieldReturn(resultSelector(outerEnumerator.current(), innerElement));
                        }

                        innerElement = null;
                        innerCount = 0;
                    }

                    if (outerEnumerator.moveNext()) {
                        const key = outerKeySelector(outerEnumerator.current());
                        innerElements = lookup.get(key).toArray();
                        // execute once if innerElements is NULL
                        if (innerElements == null || innerElements.length == 0) {
                            return this.yieldReturn(resultSelector(outerEnumerator.current(), null));
                        }
                    } else {
                        return false;
                    }
                }
            },
            function () { Utils.dispose(outerEnumerator); });
    });
};

// Overload:function (inner, outerKeySelector, innerKeySelector, resultSelector)
// Overload:function (inner, outerKeySelector, innerKeySelector, resultSelector, compareSelector)
Enumerable.prototype.groupJoin = function (inner, outerKeySelector, innerKeySelector, resultSelector, compareSelector) {
    outerKeySelector = Utils.createLambda(outerKeySelector);
    innerKeySelector = Utils.createLambda(innerKeySelector);
    resultSelector = Utils.createLambda(resultSelector);
    compareSelector = Utils.createLambda(compareSelector);
    var source = this;

    return new Enumerable(function () {
        var enumerator = source.getEnumerator();
        var lookup = null;

        return new IEnumerator(
            function () {
                enumerator = source.getEnumerator();
                lookup = Enumerable.from(inner).toLookup(innerKeySelector, Functions.Identity, compareSelector);
            },
            function () {
                if (enumerator.moveNext()) {
                    const innerElement = lookup.get(outerKeySelector(enumerator.current()));
                    return this.yieldReturn(resultSelector(enumerator.current(), innerElement));
                }
                return false;
            },
            function () { Utils.dispose(enumerator); });
    });
};

///////////////
// Set Methods

Enumerable.prototype.all = function (predicate) {
    predicate = Utils.createLambda(predicate);

    var result = true;
    this.forEach(function (x) {
        if (!predicate(x)) {
            result = false;
            return false; // break
        }
    });
    return result;
};

// Overload:function()
// Overload:function(predicate)
Enumerable.prototype.any = function (predicate) {
    predicate = Utils.createLambda(predicate);

    var enumerator = this.getEnumerator();
    try {
        if (arguments.length == 0) return enumerator.moveNext(); // case:function()

        while (enumerator.moveNext()) // case:function(predicate)
        {
            if (predicate(enumerator.current())) return true;
        }
        return false;
    }
    finally {
        Utils.dispose(enumerator);
    }
};

Enumerable.prototype.isEmpty = function () {
    return !this.any();
};

// multiple arguments
Enumerable.prototype.concat = function () {
    var source = this;

    if (arguments.length == 1) {
        const second = arguments[0];

        return new Enumerable(function () {
            var firstEnumerator;
            var secondEnumerator;

            return new IEnumerator(
                function () { firstEnumerator = source.getEnumerator(); },
                function () {
                    if (secondEnumerator == null) {
                        if (firstEnumerator.moveNext()) return this.yieldReturn(firstEnumerator.current());
                        secondEnumerator = Enumerable.from(second).getEnumerator();
                    }
                    if (secondEnumerator.moveNext()) return this.yieldReturn(secondEnumerator.current());
                    return false;
                },
                function () {
                    try {
                        Utils.dispose(firstEnumerator);
                    }
                    finally {
                        Utils.dispose(secondEnumerator);
                    }
                });
        });
    }
    else {
        const args = arguments;

        return new Enumerable(function () {
            var enumerators;

            return new IEnumerator(
                function () {
                    enumerators = Enumerable.make(source)
                        .concat(Enumerable.from(args).select(Enumerable.from))
                        .select(function (x) { return x.getEnumerator() })
                        .toArray();
                },
                function () {
                    while (enumerators.length > 0) {
                        const enumerator = enumerators[0];

                        if (enumerator.moveNext()) {
                            return this.yieldReturn(enumerator.current());
                        }
                        else {
                            enumerator.dispose();
                            enumerators.splice(0, 1);
                        }
                    }
                    return this.yieldBreak();
                },
                function () {
                    Enumerable.from(enumerators).forEach(Utils.dispose);
                });
        });
    }
};

Enumerable.prototype.insert = function (index, second) {
    var source = this;

    return new Enumerable(function () {
        var firstEnumerator;
        var secondEnumerator;
        var count = 0;
        var isEnumerated = false;

        return new IEnumerator(
            function () {
                firstEnumerator = source.getEnumerator();
                secondEnumerator = Enumerable.from(second).getEnumerator();
            },
            function () {
                if (count == index && secondEnumerator.moveNext()) {
                    isEnumerated = true;
                    return this.yieldReturn(secondEnumerator.current());
                }
                if (firstEnumerator.moveNext()) {
                    count++;
                    return this.yieldReturn(firstEnumerator.current());
                }
                if (!isEnumerated && secondEnumerator.moveNext()) {
                    return this.yieldReturn(secondEnumerator.current());
                }
                return false;
            },
            function () {
                try {
                    Utils.dispose(firstEnumerator);
                }
                finally {
                    Utils.dispose(secondEnumerator);
                }
            });
    });
};

Enumerable.prototype.alternate = function (alternateValueOrSequence) {
    var source = this;

    return new Enumerable(function () {
        var buffer;
        var enumerator;
        var alternateSequence;
        var alternateEnumerator;

        return new IEnumerator(
            function () {
                if (alternateValueOrSequence instanceof Array || alternateValueOrSequence.getEnumerator != null) {
                    alternateSequence = Enumerable.from(Enumerable.from(alternateValueOrSequence).toArray()); // freeze
                }
                else {
                    alternateSequence = Enumerable.make(alternateValueOrSequence);
                }
                enumerator = source.getEnumerator();
                if (enumerator.moveNext()) buffer = enumerator.current();
            },
            function () {
                while (true) {
                    if (alternateEnumerator != null) {
                        if (alternateEnumerator.moveNext()) {
                            return this.yieldReturn(alternateEnumerator.current());
                        }
                        else {
                            alternateEnumerator = null;
                        }
                    }

                    if (buffer == null && enumerator.moveNext()) {
                        buffer = enumerator.current(); // hasNext
                        alternateEnumerator = alternateSequence.getEnumerator();
                        continue; // GOTO
                    }
                    else if (buffer != null) {
                        const retVal = buffer;
                        buffer = null;
                        return this.yieldReturn(retVal);
                    }

                    return this.yieldBreak();
                }
            },
            function () {
                try {
                    Utils.dispose(enumerator);
                }
                finally {
                    Utils.dispose(alternateEnumerator);
                }
            });
    });
};

// Overload:function(value)
// Overload:function(value, compareSelector)
Enumerable.prototype.contains = function (value, compareSelector) {
    compareSelector = Utils.createLambda(compareSelector);
    var enumerator = this.getEnumerator();
    try {
        while (enumerator.moveNext()) {
            if (compareSelector(enumerator.current()) === value) return true;
        }
        return false;
    }
    finally {
        Utils.dispose(enumerator);
    }
};

Enumerable.prototype.defaultIfEmpty = function (defaultValue) {
    var source = this;
    if (defaultValue === undefined) defaultValue = null;

    return new Enumerable(function () {
        var enumerator;
        var isFirst = true;

        return new IEnumerator(
            function () { enumerator = source.getEnumerator(); },
            function () {
                if (enumerator.moveNext()) {
                    isFirst = false;
                    return this.yieldReturn(enumerator.current());
                }
                else if (isFirst) {
                    isFirst = false;
                    return this.yieldReturn(defaultValue);
                }
                return false;
            },
            function () { Utils.dispose(enumerator); });
    });
};

// Overload:function()
// Overload:function(compareSelector)
Enumerable.prototype.distinct = function (compareSelector) {
    return this.except(Enumerable.empty(), compareSelector);
};

Enumerable.prototype.distinctUntilChanged = function (compareSelector) {
    compareSelector = Utils.createLambda(compareSelector);
    var source = this;

    return new Enumerable(function () {
        var enumerator;
        var compareKey;
        var initial;

        return new IEnumerator(
            function () {
                enumerator = source.getEnumerator();
            },
            function () {
                while (enumerator.moveNext()) {
                    const key = compareSelector(enumerator.current());

                    if (initial) {
                        initial = false;
                        compareKey = key;
                        return this.yieldReturn(enumerator.current());
                    }

                    if (compareKey === key) {
                        continue;
                    }

                    compareKey = key;
                    return this.yieldReturn(enumerator.current());
                }
                return this.yieldBreak();
            },
            function () { Utils.dispose(enumerator); });
    });
};

// Overload:function(second)
// Overload:function(second, compareSelector)
Enumerable.prototype.except = function (second, compareSelector) {
    compareSelector = Utils.createLambda(compareSelector);
    var source = this;

    return new Enumerable(function () {
        var enumerator;
        var keys;

        return new IEnumerator(
            function () {
                enumerator = source.getEnumerator();
                keys = new Dictionary(compareSelector);
                Enumerable.from(second).forEach(function (key) { keys.add(key); });
            },
            function () {
                while (enumerator.moveNext()) {
                    const current = enumerator.current();
                    if (!keys.contains(current)) {
                        keys.add(current);
                        return this.yieldReturn(current);
                    }
                }
                return false;
            },
            function () { Utils.dispose(enumerator); });
    });
};

// Overload:function(second)
// Overload:function(second, compareSelector)
Enumerable.prototype.intersect = function (second, compareSelector) {
    compareSelector = Utils.createLambda(compareSelector);
    var source = this;

    return new Enumerable(function () {
        var enumerator;
        var keys;
        var outs;

        return new IEnumerator(
            function () {
                enumerator = source.getEnumerator();

                keys = new Dictionary(compareSelector);
                Enumerable.from(second).forEach(function (key) { keys.add(key); });
                outs = new Dictionary(compareSelector);
            },
            function () {
                while (enumerator.moveNext()) {
                    const current = enumerator.current();
                    if (!outs.contains(current) && keys.contains(current)) {
                        outs.add(current);
                        return this.yieldReturn(current);
                    }
                }
                return false;
            },
            function () { Utils.dispose(enumerator); });
    });
};

// Overload:function(second)
// Overload:function(second, compareSelector)
Enumerable.prototype.sequenceEqual = function (second, compareSelector) {
    compareSelector = Utils.createLambda(compareSelector);

    var firstEnumerator = this.getEnumerator();
    try {
        const secondEnumerator = Enumerable.from(second).getEnumerator();
        try {
            while (firstEnumerator.moveNext()) {
                if (!secondEnumerator.moveNext()
                    || compareSelector(firstEnumerator.current()) !== compareSelector(secondEnumerator.current())) {
                    return false;
                }
            }

            if (secondEnumerator.moveNext()) return false;
            return true;
        }
        finally {
            Utils.dispose(secondEnumerator);
        }
    }
    finally {
        Utils.dispose(firstEnumerator);
    }
};

Enumerable.prototype.union = function (second, compareSelector) {
    compareSelector = Utils.createLambda(compareSelector);
    var source = this;

    return new Enumerable(function () {
        var firstEnumerator;
        var secondEnumerator;
        var keys;

        return new IEnumerator(
            function () {
                firstEnumerator = source.getEnumerator();
                keys = new Dictionary(compareSelector);
            },
            function () {
                var current;
                if (secondEnumerator === undefined) {
                    while (firstEnumerator.moveNext()) {
                        current = firstEnumerator.current();
                        if (!keys.contains(current)) {
                            keys.add(current);
                            return this.yieldReturn(current);
                        }
                    }
                    secondEnumerator = Enumerable.from(second).getEnumerator();
                }
                while (secondEnumerator.moveNext()) {
                    current = secondEnumerator.current();
                    if (!keys.contains(current)) {
                        keys.add(current);
                        return this.yieldReturn(current);
                    }
                }
                return false;
            },
            function () {
                try {
                    Utils.dispose(firstEnumerator);
                }
                finally {
                    Utils.dispose(secondEnumerator);
                }
            });
    });
};

////////////////////
// Ordering Methods

Enumerable.prototype.orderBy = function (keySelector, comparer) {
    return new OrderedEnumerable(this, keySelector, comparer, false);
};

Enumerable.prototype.orderByDescending = function (keySelector, comparer) {
    return new OrderedEnumerable(this, keySelector, comparer, true);
};

Enumerable.prototype.reverse = function () {
    var source = this;

    return new Enumerable(function () {
        var buffer;
        var index;

        return new IEnumerator(
            function () {
                buffer = source.toArray();
                index = buffer.length;
            },
            function () {
                return (index > 0)
                    ? this.yieldReturn(buffer[--index])
                    : false;
            },
            Functions.Blank);
    });
};

Enumerable.prototype.shuffle = function () {
    var source = this;

    return new Enumerable(function () {
        var buffer;

        return new IEnumerator(
            function () { buffer = source.toArray(); },
            function () {
                if (buffer.length > 0) {
                    const i = Math.floor(Math.random() * buffer.length);
                    return this.yieldReturn(buffer.splice(i, 1)[0]);
                }
                return false;
            },
            Functions.Blank);
    });
};

Enumerable.prototype.weightedSample = function (weightSelector) {
    weightSelector = Utils.createLambda(weightSelector);
    var source = this;

    return new Enumerable(function () {
        var sortedByBound;
        var totalWeight = 0;

        return new IEnumerator(
            function () {
                sortedByBound = source
                    .choose(function (x) {
                        var weight = weightSelector(x);
                        if (weight <= 0) return null; // ignore 0

                        totalWeight += weight;
                        return { value: x, bound: totalWeight };
                    })
                    .toArray();
            },
            function () {
                if (sortedByBound.length > 0) {
                    const draw = Math.floor(Math.random() * totalWeight) + 1;

                    let lower = -1;
                    let upper = sortedByBound.length;
                    while (upper - lower > 1) {
                        const index = Math.floor((lower + upper) / 2);
                        if (sortedByBound[index].bound >= draw) {
                            upper = index;
                        }
                        else {
                            lower = index;
                        }
                    }

                    return this.yieldReturn(sortedByBound[upper].value);
                }

                return this.yieldBreak();
            },
            Functions.Blank);
    });
};

////////////////////
// Grouping Methods

// Overload:function(keySelector)
// Overload:function(keySelector,elementSelector)
// Overload:function(keySelector,elementSelector,resultSelector)
// Overload:function(keySelector,elementSelector,resultSelector,compareSelector)
Enumerable.prototype.groupBy = function (keySelector, elementSelector, resultSelector, compareSelector) {
    var source = this;
    keySelector = Utils.createLambda(keySelector);
    elementSelector = Utils.createLambda(elementSelector);
    if (resultSelector != null) resultSelector = Utils.createLambda(resultSelector);
    compareSelector = Utils.createLambda(compareSelector);

    return new Enumerable(function () {
        var enumerator;

        return new IEnumerator(
            function () {
                enumerator = source.toLookup(keySelector, elementSelector, compareSelector)
                    .toEnumerable()
                    .getEnumerator();
            },
            function () {
                while (enumerator.moveNext()) {
                    return (resultSelector == null)
                        ? this.yieldReturn(enumerator.current())
                        : this.yieldReturn(resultSelector(enumerator.current().key(), enumerator.current()));
                }
                return false;
            },
            function () { Utils.dispose(enumerator); });
    });
};

// Overload:function(keySelector)
// Overload:function(keySelector,elementSelector)
// Overload:function(keySelector,elementSelector,resultSelector)
// Overload:function(keySelector,elementSelector,resultSelector,compareSelector)
Enumerable.prototype.partitionBy = function (keySelector, elementSelector, resultSelector, compareSelector) {
    var source = this;
    keySelector = Utils.createLambda(keySelector);
    elementSelector = Utils.createLambda(elementSelector);
    compareSelector = Utils.createLambda(compareSelector);
    var hasResultSelector;
    if (resultSelector == null) {
        hasResultSelector = false;
        resultSelector = function (key, group) { return new Grouping(key, group); };
    }
    else {
        hasResultSelector = true;
        resultSelector = Utils.createLambda(resultSelector);
    }

    return new Enumerable(function () {
        var enumerator;
        var key;
        var compareKey;
        var group = [];

        return new IEnumerator(
            function () {
                enumerator = source.getEnumerator();
                if (enumerator.moveNext()) {
                    key = keySelector(enumerator.current());
                    compareKey = compareSelector(key);
                    group.push(elementSelector(enumerator.current()));
                }
            },
            function () {
                var hasNext;
                while ((hasNext = enumerator.moveNext()) == true) {
                    if (compareKey === compareSelector(keySelector(enumerator.current()))) {
                        group.push(elementSelector(enumerator.current()));
                    }
                    else break;
                }

                if (group.length > 0) {
                    const result = (hasResultSelector)
                        ? resultSelector(key, Enumerable.from(group))
                        : resultSelector(key, group);
                    if (hasNext) {
                        key = keySelector(enumerator.current());
                        compareKey = compareSelector(key);
                        group = [elementSelector(enumerator.current())];
                    }
                    else group = [];

                    return this.yieldReturn(result);
                }

                return false;
            },
            function () { Utils.dispose(enumerator); });
    });
};

Enumerable.prototype.buffer = function (count) {
    var source = this;

    return new Enumerable(function () {
        var enumerator;

        return new IEnumerator(
            function () { enumerator = source.getEnumerator(); },
            function () {
                var array = [];
                var index = 0;
                while (enumerator.moveNext()) {
                    array.push(enumerator.current());
                    if (++index >= count) return this.yieldReturn(array);
                }
                if (array.length > 0) return this.yieldReturn(array);
                return false;
            },
            function () { Utils.dispose(enumerator); });
    });
};

/////////////////////
// Aggregate Methods

// Overload:function(func)
// Overload:function(seed,func)
// Overload:function(seed,func,resultSelector)
Enumerable.prototype.aggregate = function (seed, func, resultSelector) {
    resultSelector = Utils.createLambda(resultSelector);
    return resultSelector(this.scan(seed, func, resultSelector).last());
};

// Overload:function()
// Overload:function(selector)
Enumerable.prototype.average = function (selector) {
    selector = Utils.createLambda(selector);

    var sum = 0;
    var count = 0;
    this.forEach(function (x) {
        sum += selector(x);
        ++count;
    });

    return sum / count;
};

// Overload:function()
// Overload:function(predicate)
Enumerable.prototype.count = function (predicate) {
    predicate = (predicate == null) ? Functions.True : Utils.createLambda(predicate);

    var count = 0;
    this.forEach(function (x, i) {
        if (predicate(x, i)) ++count;
    });
    return count;
};

// Overload:function()
// Overload:function(selector)
Enumerable.prototype.max = function (selector) {
    if (selector == null) selector = Functions.Identity;
    return this.select(selector).aggregate(function (a, b) { return (a > b) ? a : b; });
};

// Overload:function()
// Overload:function(selector)
Enumerable.prototype.min = function (selector) {
    if (selector == null) selector = Functions.Identity;
    return this.select(selector).aggregate(function (a, b) { return (a < b) ? a : b; });
};

Enumerable.prototype.maxBy = function (keySelector) {
    keySelector = Utils.createLambda(keySelector);
    return this.aggregate(function (a, b) { return (keySelector(a) > keySelector(b)) ? a : b; });
};

Enumerable.prototype.minBy = function (keySelector) {
    keySelector = Utils.createLambda(keySelector);
    return this.aggregate(function (a, b) { return (keySelector(a) < keySelector(b)) ? a : b; });
};

// Overload:function()
// Overload:function(selector)
Enumerable.prototype.sum = function (selector) {
    if (selector == null) selector = Functions.Identity;
    return this.select(selector).aggregate(0, function (a, b) { return a + b; });
};

//////////////////
// Paging Methods

Enumerable.prototype.elementAt = function (index) {
    var value;
    var found = false;
    this.forEach(function (x, i) {
        if (i == index) {
            value = x;
            found = true;
            return false;
        }
    });

    if (!found) throw new Error("index is less than 0 or greater than or equal to the number of elements in source.");
    return value;
};

Enumerable.prototype.elementAtOrDefault = function (index, defaultValue) {
    if (defaultValue === undefined) defaultValue = null;
    var value;
    var found = false;
    this.forEach(function (x, i) {
        if (i == index) {
            value = x;
            found = true;
            return false;
        }
    });

    return (!found) ? defaultValue : value;
};

// Overload:function()
// Overload:function(predicate)
Enumerable.prototype.first = function (predicate) {
    if (predicate != null) return this.where(predicate).first();

    var value;
    var found = false;
    this.forEach(function (x) {
        value = x;
        found = true;
        return false;
    });

    if (!found) throw new Error("first:No element satisfies the condition.");
    return value;
};

Enumerable.prototype.firstOrDefault = function (predicate, defaultValue) {
    if (predicate !== undefined) {
        if (typeof predicate === Types.Function || typeof Utils.createLambda(predicate) === Types.Function) {
            return this.where(predicate).firstOrDefault(undefined, defaultValue);
        }
        defaultValue = predicate;
    }

    var value;
    var found = false;
    this.forEach(function (x) {
        value = x;
        found = true;
        return false;
    });
    return (!found) ? defaultValue : value;
};

// Overload:function()
// Overload:function(predicate)
Enumerable.prototype.last = function (predicate) {
    if (predicate != null) return this.where(predicate).last();

    var value;
    var found = false;
    this.forEach(function (x) {
        found = true;
        value = x;
    });

    if (!found) throw new Error("last:No element satisfies the condition.");
    return value;
};

Enumerable.prototype.lastOrDefault = function (predicate, defaultValue) {
    if (predicate !== undefined) {
        if (typeof predicate === Types.Function || typeof Utils.createLambda(predicate) === Types.Function) {
            return this.where(predicate).lastOrDefault(undefined, defaultValue);
        }
        defaultValue = predicate;
    }

    var value;
    var found = false;
    this.forEach(function (x) {
        found = true;
        value = x;
    });
    return (!found) ? defaultValue : value;
};

// Overload:function()
// Overload:function(predicate)
Enumerable.prototype.single = function (predicate) {
    if (predicate != null) return this.where(predicate).single();

    var value;
    var found = false;
    this.forEach(function (x) {
        if (!found) {
            found = true;
            value = x;
        } else throw new Error("single:sequence contains more than one element.");
    });

    if (!found) throw new Error("single:No element satisfies the condition.");
    return value;
};

// Overload:function(defaultValue)
// Overload:function(defaultValue,predicate)
Enumerable.prototype.singleOrDefault = function (predicate, defaultValue) {
    if (defaultValue === undefined) defaultValue = null;
    if (predicate != null) return this.where(predicate).singleOrDefault(null, defaultValue);

    var value;
    var found = false;
    this.forEach(function (x) {
        if (!found) {
            found = true;
            value = x;
        } else throw new Error("single:sequence contains more than one element.");
    });

    return (!found) ? defaultValue : value;
};

Enumerable.prototype.skip = function (count) {
    var source = this;

    return new Enumerable(function () {
        var enumerator;
        var index = 0;

        return new IEnumerator(
            function () {
                enumerator = source.getEnumerator();
                while (index++ < count && enumerator.moveNext()) { }
            },
            function () {
                return (enumerator.moveNext())
                    ? this.yieldReturn(enumerator.current())
                    : false;
            },
            function () { Utils.dispose(enumerator); });
    });
};

// Overload:function(predicate<element>)
// Overload:function(predicate<element,index>)
Enumerable.prototype.skipWhile = function (predicate) {
    predicate = Utils.createLambda(predicate);
    var source = this;

    return new Enumerable(function () {
        var enumerator;
        var index = 0;
        var isSkipEnd = false;

        return new IEnumerator(
            function () { enumerator = source.getEnumerator(); },
            function () {
                while (!isSkipEnd) {
                    if (enumerator.moveNext()) {
                        if (!predicate(enumerator.current(), index++)) {
                            isSkipEnd = true;
                            return this.yieldReturn(enumerator.current());
                        }
                        continue;
                    } else return false;
                }

                return (enumerator.moveNext())
                    ? this.yieldReturn(enumerator.current())
                    : false;

            },
            function () { Utils.dispose(enumerator); });
    });
};

Enumerable.prototype.take = function (count) {
    var source = this;

    return new Enumerable(function () {
        var enumerator;
        var index = 0;

        return new IEnumerator(
            function () { enumerator = source.getEnumerator(); },
            function () {
                return (index++ < count && enumerator.moveNext())
                    ? this.yieldReturn(enumerator.current())
                    : false;
            },
            function () { Utils.dispose(enumerator); }
        );
    });
};

// Overload:function(predicate<element>)
// Overload:function(predicate<element,index>)
Enumerable.prototype.takeWhile = function (predicate) {
    predicate = Utils.createLambda(predicate);
    var source = this;

    return new Enumerable(function () {
        var enumerator;
        var index = 0;

        return new IEnumerator(
            function () { enumerator = source.getEnumerator(); },
            function () {
                return (enumerator.moveNext() && predicate(enumerator.current(), index++))
                    ? this.yieldReturn(enumerator.current())
                    : false;
            },
            function () { Utils.dispose(enumerator); });
    });
};

// Overload:function()
// Overload:function(count)
Enumerable.prototype.takeExceptLast = function (count) {
    if (count == null) count = 1;
    var source = this;

    return new Enumerable(function () {
        if (count <= 0) return source.getEnumerator(); // do nothing

        var enumerator;
        var q = [];

        return new IEnumerator(
            function () { enumerator = source.getEnumerator(); },
            function () {
                while (enumerator.moveNext()) {
                    if (q.length == count) {
                        q.push(enumerator.current());
                        return this.yieldReturn(q.shift());
                    }
                    q.push(enumerator.current());
                }
                return false;
            },
            function () { Utils.dispose(enumerator); });
    });
};

Enumerable.prototype.takeFromLast = function (count) {
    if (count <= 0 || count == null) return Enumerable.empty();
    var source = this;

    return new Enumerable(function () {
        var sourceEnumerator;
        var enumerator;
        var q = [];

        return new IEnumerator(
            function () { sourceEnumerator = source.getEnumerator(); },
            function () {
                while (sourceEnumerator.moveNext()) {
                    if (q.length == count) q.shift();
                    q.push(sourceEnumerator.current());
                }
                if (enumerator == null) {
                    enumerator = Enumerable.from(q).getEnumerator();
                }
                return (enumerator.moveNext())
                    ? this.yieldReturn(enumerator.current())
                    : false;
            },
            function () { Utils.dispose(enumerator); });
    });
};

// Overload:function(item)
// Overload:function(predicate)
Enumerable.prototype.indexOf = function (item) {
    var found = null;

    // item as predicate
    if (typeof (item) === Types.Function) {
        this.forEach(function (x, i) {
            if (item(x, i)) {
                found = i;
                return false;
            }
        });
    }
    else {
        this.forEach(function (x, i) {
            if (x === item) {
                found = i;
                return false;
            }
        });
    }

    return (found !== null) ? found : -1;
};

// Overload:function(item)
// Overload:function(predicate)
Enumerable.prototype.lastIndexOf = function (item) {
    var result = -1;

    // item as predicate
    if (typeof (item) === Types.Function) {
        this.forEach(function (x, i) {
            if (item(x, i)) result = i;
        });
    }
    else {
        this.forEach(function (x, i) {
            if (x === item) result = i;
        });
    }

    return result;
};

///////////////////
// Convert Methods

Enumerable.prototype.cast = function () {
    return this;
};

Enumerable.prototype.asEnumerable = function () {
    return Enumerable.from(this);
};

Enumerable.prototype.toArray = function () {
    var array = [];
    this.forEach(function (x) { array.push(x); });
    return array;
};

// Overload:function(keySelector)
// Overload:function(keySelector, elementSelector)
// Overload:function(keySelector, elementSelector, compareSelector)
Enumerable.prototype.toLookup = function (keySelector, elementSelector, compareSelector) {
    keySelector = Utils.createLambda(keySelector);
    elementSelector = Utils.createLambda(elementSelector);
    compareSelector = Utils.createLambda(compareSelector);

    var dict = new Dictionary(compareSelector);
    this.forEach(function (x) {
        var key = keySelector(x);
        var element = elementSelector(x);

        var array = dict.get(key);
        if (array !== undefined) array.push(element);
        else dict.add(key, [element]);
    });
    return new Lookup(dict);
};

Enumerable.prototype.toObject = function (keySelector, elementSelector) {
    keySelector = Utils.createLambda(keySelector);
    elementSelector = Utils.createLambda(elementSelector);

    var obj = {};
    this.forEach(function (x) {
        obj[keySelector(x)] = elementSelector(x);
    });
    return obj;
};

// Overload:function(keySelector, elementSelector)
// Overload:function(keySelector, elementSelector, compareSelector)
Enumerable.prototype.toDictionary = function (keySelector, elementSelector, compareSelector) {
    keySelector = Utils.createLambda(keySelector);
    elementSelector = Utils.createLambda(elementSelector);
    compareSelector = Utils.createLambda(compareSelector);

    var dict = new Dictionary(compareSelector);
    this.forEach(function (x) {
        dict.add(keySelector(x), elementSelector(x));
    });
    return dict;
};

// Overload:function()
// Overload:function(replacer)
// Overload:function(replacer, space)
Enumerable.prototype.toJSONString = function (replacer, space) {
    if (typeof JSON === Types.Undefined || JSON.stringify == null) {
        throw new Error("toJSONString can't find JSON.stringify. This works native JSON support Browser or include json2.js");
    }
    return JSON.stringify(this.toArray(), replacer, space);
};

// Overload:function()
// Overload:function(separator)
// Overload:function(separator,selector)
Enumerable.prototype.toJoinedString = function (separator, selector) {
    if (separator == null) separator = "";
    if (selector == null) selector = Functions.Identity;

    return this.select(selector).toArray().join(separator);
};

//////////////////
// Action Methods

// Overload:function(action<element>)
// Overload:function(action<element,index>)
Enumerable.prototype.doAction = function (action) {
    var source = this;
    action = Utils.createLambda(action);

    return new Enumerable(function () {
        var enumerator;
        var index = 0;

        return new IEnumerator(
            function () { enumerator = source.getEnumerator(); },
            function () {
                if (enumerator.moveNext()) {
                    action(enumerator.current(), index++);
                    return this.yieldReturn(enumerator.current());
                }
                return false;
            },
            function () { Utils.dispose(enumerator); });
    });
};

// Overload:function(action<element>)
// Overload:function(action<element,index>)
// Overload:function(func<element,bool>)
// Overload:function(func<element,index,bool>)
Enumerable.prototype.forEach = function (action) {
    action = Utils.createLambda(action);

    var index = 0;
    var enumerator = this.getEnumerator();
    try {
        while (enumerator.moveNext()) {
            if (action(enumerator.current(), index++) === false) break;
        }
    } finally {
        Utils.dispose(enumerator);
    }
};

Enumerable.prototype.force = function () {
    var enumerator = this.getEnumerator();

    try {
        while (enumerator.moveNext()) { }
    }
    finally {
        Utils.dispose(enumerator);
    }
};

//////////////////////
// Functional Methods

Enumerable.prototype.letBind = function (func) {
    func = Utils.createLambda(func);
    var source = this;

    return new Enumerable(function () {
        var enumerator;

        return new IEnumerator(
            function () {
                enumerator = Enumerable.from(func(source)).getEnumerator();
            },
            function () {
                return (enumerator.moveNext())
                    ? this.yieldReturn(enumerator.current())
                    : false;
            },
            function () { Utils.dispose(enumerator); });
    });
};

Enumerable.prototype.share = function () {
    var source = this;
    var sharedEnumerator;
    var disposed = false;

    return new DisposableEnumerable(function () {
        return new IEnumerator(
            function () {
                if (sharedEnumerator == null) {
                    sharedEnumerator = source.getEnumerator();
                }
            },
            function () {
                if (disposed) throw new Error("enumerator is disposed");

                return (sharedEnumerator.moveNext())
                    ? this.yieldReturn(sharedEnumerator.current())
                    : false;
            },
            Functions.Blank
        );
    }, function () {
        disposed = true;
        Utils.dispose(sharedEnumerator);
    });
};

Enumerable.prototype.memoize = function () {
    var source = this;
    var cache;
    var enumerator;
    var disposed = false;

    return new DisposableEnumerable(function () {
        var index = -1;

        return new IEnumerator(
            function () {
                if (enumerator == null) {
                    enumerator = source.getEnumerator();
                    cache = [];
                }
            },
            function () {
                if (disposed) throw new Error("enumerator is disposed");

                index++;
                if (cache.length <= index) {
                    return (enumerator.moveNext())
                        ? this.yieldReturn(cache[index] = enumerator.current())
                        : false;
                }

                return this.yieldReturn(cache[index]);
            },
            Functions.Blank
        );
    }, function () {
        disposed = true;
        Utils.dispose(enumerator);
        cache = null;
    });
};

// Iterator support (ES6 for..of)
if (Utils.hasNativeIteratorSupport()) {
    Enumerable.prototype[Symbol.iterator] = function () {
        return {
            enumerator: this.getEnumerator(),
            next: function () {
                if (this.enumerator.moveNext()) {
                    return {
                        done: false,
                        value: this.enumerator.current()
                    };
                } else {
                    return { done: true };
                }
            }
        };
    };
}

//////////////////////////
// Error Handling Methods

Enumerable.prototype.catchError = function (handler) {
    handler = Utils.createLambda(handler);
    var source = this;

    return new Enumerable(function () {
        var enumerator;

        return new IEnumerator(
            function () { enumerator = source.getEnumerator(); },
            function () {
                try {
                    return (enumerator.moveNext())
                        ? this.yieldReturn(enumerator.current())
                        : false;
                } catch (e) {
                    handler(e);
                    return false;
                }
            },
            function () { Utils.dispose(enumerator); });
    });
};

Enumerable.prototype.finallyAction = function (finallyAction) {
    finallyAction = Utils.createLambda(finallyAction);
    var source = this;

    return new Enumerable(function () {
        var enumerator;

        return new IEnumerator(
            function () { enumerator = source.getEnumerator(); },
            function () {
                return (enumerator.moveNext())
                    ? this.yieldReturn(enumerator.current())
                    : false;
            },
            function () {
                try {
                    Utils.dispose(enumerator);
                } finally {
                    finallyAction();
                }
            });
    });
};

/////////////////
// Debug Methods

// Overload:function()
// Overload:function(selector)
Enumerable.prototype.log = function (selector) {
    selector = Utils.createLambda(selector);

    return this.doAction(function (item) {
        if (typeof console !== Types.Undefined) {
            console.log(selector(item));
        }
    });
};

// Overload:function()
// Overload:function(message)
// Overload:function(message,selector)
Enumerable.prototype.trace = function (message, selector) {
    if (message == null) message = "Trace";
    selector = Utils.createLambda(selector);

    return this.doAction(function (item) {
        if (typeof console !== Types.Undefined) {
            console.log(message, selector(item));
        }
    });
};

///////////
// Private

var OrderedEnumerable = function (source, keySelector, comparer, descending, parent) {
    this.source = source;
    this.keySelector = Utils.createLambda(keySelector);
    this.descending = descending;
    this.parent = parent;

    if (comparer)
        this.comparer = Utils.createLambda(comparer);
};
OrderedEnumerable.prototype = new Enumerable();

OrderedEnumerable.prototype.createOrderedEnumerable = function (keySelector, comparer, descending) {
    return new OrderedEnumerable(this.source, keySelector, comparer, descending, this);
};

OrderedEnumerable.prototype.thenBy = function (keySelector, comparer) {
    return this.createOrderedEnumerable(keySelector, comparer, false);
};

OrderedEnumerable.prototype.thenByDescending = function (keySelector, comparer) {
    return this.createOrderedEnumerable(keySelector, comparer, true);
};

OrderedEnumerable.prototype.getEnumerator = function () {
    var self = this;
    var buffer;
    var indexes;
    var index = 0;

    return new IEnumerator(
        function () {
            buffer = [];
            indexes = [];
            self.source.forEach(function (item, index) {
                buffer.push(item);
                indexes.push(index);
            });
            var sortContext = SortContext.create(self, null);
            sortContext.GenerateKeys(buffer);

            indexes.sort(function (a, b) { return sortContext.compare(a, b); });
        },
        function () {
            return (index < indexes.length)
                ? this.yieldReturn(buffer[indexes[index++]])
                : false;
        },
        Functions.Blank
    );
};

var SortContext = function (keySelector, comparer, descending, child) {
    this.keySelector = keySelector;
    this.descending = descending;
    this.child = child;
    this.comparer = comparer;
    this.keys = null;
};

SortContext.create = function (orderedEnumerable, currentContext) {
    var context = new SortContext(
        orderedEnumerable.keySelector, orderedEnumerable.comparer, orderedEnumerable.descending, currentContext
    );

    if (orderedEnumerable.parent != null) return SortContext.create(orderedEnumerable.parent, context);
    return context;
};

SortContext.prototype.GenerateKeys = function (source) {
    var len = source.length;
    var keySelector = this.keySelector;
    var keys = new Array(len);
    for (let i = 0; i < len; i++) keys[i] = keySelector(source[i]);
    this.keys = keys;

    if (this.child != null) this.child.GenerateKeys(source);
};

SortContext.prototype.compare = function (index1, index2) {
    var comparison = this.comparer ?
        this.comparer(this.keys[index1], this.keys[index2]) :
        Utils.compare(this.keys[index1], this.keys[index2]);

    if (comparison == 0) {
        if (this.child != null) return this.child.compare(index1, index2);
        return Utils.compare(index1, index2);
    }

    return (this.descending) ? -comparison : comparison;
};

var DisposableEnumerable = function (getEnumerator, dispose) {
    this.dispose = dispose;
    Enumerable.call(this, getEnumerator);
};
DisposableEnumerable.prototype = new Enumerable();

var ArrayEnumerable = function (source) {
    this.getSource = function () { return source; };
};
ArrayEnumerable.prototype = new Enumerable();

ArrayEnumerable.prototype.any = function (predicate) {
    return (predicate == null)
        ? (this.getSource().length > 0)
        : Enumerable.prototype.any.apply(this, arguments);
};

ArrayEnumerable.prototype.count = function (predicate) {
    return (predicate == null)
        ? this.getSource().length
        : Enumerable.prototype.count.apply(this, arguments);
};

ArrayEnumerable.prototype.elementAt = function (index) {
    var source = this.getSource();
    return (0 <= index && index < source.length)
        ? source[index]
        : Enumerable.prototype.elementAt.apply(this, arguments);
};

ArrayEnumerable.prototype.elementAtOrDefault = function (index, defaultValue) {
    if (defaultValue === undefined) defaultValue = null;
    var source = this.getSource();
    return (0 <= index && index < source.length)
        ? source[index]
        : defaultValue;
};

ArrayEnumerable.prototype.first = function (predicate) {
    var source = this.getSource();
    return (predicate == null && source.length > 0)
        ? source[0]
        : Enumerable.prototype.first.apply(this, arguments);
};

ArrayEnumerable.prototype.firstOrDefault = function (predicate, defaultValue) {
    if (predicate !== undefined) {
        return Enumerable.prototype.firstOrDefault.apply(this, arguments);
    }
    defaultValue = predicate;

    var source = this.getSource();
    return source.length > 0 ? source[0] : defaultValue;
};

ArrayEnumerable.prototype.last = function (predicate) {
    var source = this.getSource();
    return (predicate == null && source.length > 0)
        ? source[source.length - 1]
        : Enumerable.prototype.last.apply(this, arguments);
};

ArrayEnumerable.prototype.lastOrDefault = function (predicate, defaultValue) {
    if (predicate !== undefined) {
        return Enumerable.prototype.lastOrDefault.apply(this, arguments);
    }
    defaultValue = predicate;

    var source = this.getSource();
    return source.length > 0 ? source[source.length - 1] : defaultValue;
};

ArrayEnumerable.prototype.skip = function (count) {
    var source = this.getSource();

    return new Enumerable(function () {
        var index;

        return new IEnumerator(
            function () { index = (count < 0) ? 0 : count; },
            function () {
                return (index < source.length)
                    ? this.yieldReturn(source[index++])
                    : false;
            },
            Functions.Blank);
    });
};

ArrayEnumerable.prototype.takeExceptLast = function (count) {
    if (count == null) count = 1;
    return this.take(this.getSource().length - count);
};

ArrayEnumerable.prototype.takeFromLast = function (count) {
    return this.skip(this.getSource().length - count);
};

ArrayEnumerable.prototype.reverse = function () {
    var source = this.getSource();

    return new Enumerable(function () {
        var index;

        return new IEnumerator(
            function () {
                index = source.length;
            },
            function () {
                return (index > 0)
                    ? this.yieldReturn(source[--index])
                    : false;
            },
            Functions.Blank);
    });
};

ArrayEnumerable.prototype.sequenceEqual = function (second, compareSelector) {
    if ((second instanceof ArrayEnumerable || second instanceof Array)
        && compareSelector == null
        && Enumerable.from(second).count() != this.count()) {
        return false;
    }

    return Enumerable.prototype.sequenceEqual.apply(this, arguments);
};

ArrayEnumerable.prototype.toJoinedString = function (separator, selector) {
    var source = this.getSource();
    if (selector != null || !(source instanceof Array)) {
        return Enumerable.prototype.toJoinedString.apply(this, arguments);
    }

    if (separator == null) separator = "";
    return source.join(separator);
};

ArrayEnumerable.prototype.getEnumerator = function () {
    var source = this.getSource();
    var index = -1;

    // fast and simple enumerator
    return {
        current: function () { return source[index]; },
        moveNext: function () {
            return ++index < source.length;
        },
        dispose: Functions.Blank
    };
};

// optimization for multiple where and multiple select and whereselect

var WhereEnumerable = function (source, predicate) {
    this.prevSource = source;
    this.prevPredicate = predicate; // predicate.length always <= 1
};
WhereEnumerable.prototype = new Enumerable();

WhereEnumerable.prototype.where = function (predicate) {
    predicate = Utils.createLambda(predicate);

    if (predicate.length <= 1) {
        const prevPredicate = this.prevPredicate;
        const composedPredicate = function (x) { return prevPredicate(x) && predicate(x); };
        return new WhereEnumerable(this.prevSource, composedPredicate);
    }
    else {
        // if predicate use index, can't compose
        return Enumerable.prototype.where.call(this, predicate);
    }
};

WhereEnumerable.prototype.select = function (selector) {
    selector = Utils.createLambda(selector);

    return (selector.length <= 1)
        ? new WhereSelectEnumerable(this.prevSource, this.prevPredicate, selector)
        : Enumerable.prototype.select.call(this, selector);
};

WhereEnumerable.prototype.getEnumerator = function () {
    var predicate = this.prevPredicate;
    var source = this.prevSource;
    var enumerator;

    return new IEnumerator(
        function () { enumerator = source.getEnumerator(); },
        function () {
            while (enumerator.moveNext()) {
                if (predicate(enumerator.current())) {
                    return this.yieldReturn(enumerator.current());
                }
            }
            return false;
        },
        function () { Utils.dispose(enumerator); });
};

var WhereSelectEnumerable = function (source, predicate, selector) {
    this.prevSource = source;
    this.prevPredicate = predicate; // predicate.length always <= 1 or null
    this.prevSelector = selector; // selector.length always <= 1
};
WhereSelectEnumerable.prototype = new Enumerable();

WhereSelectEnumerable.prototype.where = function (predicate) {
    predicate = Utils.createLambda(predicate);

    return (predicate.length <= 1)
        ? new WhereEnumerable(this, predicate)
        : Enumerable.prototype.where.call(this, predicate);
};

WhereSelectEnumerable.prototype.select = function (selector) {
    selector = Utils.createLambda(selector);

    if (selector.length <= 1) {
        const prevSelector = this.prevSelector;
        const composedSelector = function (x) { return selector(prevSelector(x)); };
        return new WhereSelectEnumerable(this.prevSource, this.prevPredicate, composedSelector);
    }
    else {
        // if selector uses index, can't compose
        return Enumerable.prototype.select.call(this, selector);
    }
};

WhereSelectEnumerable.prototype.getEnumerator = function () {
    var predicate = this.prevPredicate;
    var selector = this.prevSelector;
    var source = this.prevSource;
    var enumerator;

    return new IEnumerator(
        function () { enumerator = source.getEnumerator(); },
        function () {
            while (enumerator.moveNext()) {
                if (predicate == null || predicate(enumerator.current())) {
                    return this.yieldReturn(selector(enumerator.current()));
                }
            }
            return false;
        },
        function () { Utils.dispose(enumerator); });
};

///////////////
// Collections

var Dictionary = (function () {
    // static utility methods
    var callHasOwnProperty = function (target, key) {
        return Object.prototype.hasOwnProperty.call(target, key);
    };

    var computeHashCode = function (obj) {
        if (obj === null) return "null";
        if (obj === undefined) return "undefined";

        return (typeof obj.toString === Types.Function)
            ? obj.toString()
            : Object.prototype.toString.call(obj);
    };

    // LinkedList for Dictionary
    var HashEntry = function (key, value) {
        this.key = key;
        this.value = value;
        this.prev = null;
        this.next = null;
    };

    var EntryList = function () {
        this.first = null;
        this.last = null;
    };
    EntryList.prototype =
    {
        addLast: function (entry) {
            if (this.last != null) {
                this.last.next = entry;
                entry.prev = this.last;
                this.last = entry;
            } else this.first = this.last = entry;
        },

        replace: function (entry, newEntry) {
            if (entry.prev != null) {
                entry.prev.next = newEntry;
                newEntry.prev = entry.prev;
            } else this.first = newEntry;

            if (entry.next != null) {
                entry.next.prev = newEntry;
                newEntry.next = entry.next;
            } else this.last = newEntry;

        },

        remove: function (entry) {
            if (entry.prev != null) entry.prev.next = entry.next;
            else this.first = entry.next;

            if (entry.next != null) entry.next.prev = entry.prev;
            else this.last = entry.prev;
        }
    };

    // Overload:function()
    // Overload:function(compareSelector)
    var Dictionary = function (compareSelector) {
        this.countField = 0;
        this.entryList = new EntryList();
        this.buckets = {}; // as Dictionary<string,List<object>>
        this.compareSelector = (compareSelector == null) ? Functions.Identity : compareSelector;
    };
    Dictionary.prototype =
    {
        add: function (key, value) {
            var compareKey = this.compareSelector(key);
            var hash = computeHashCode(compareKey);
            var entry = new HashEntry(key, value);
            if (callHasOwnProperty(this.buckets, hash)) {
                const array = this.buckets[hash];
                for (let i = 0; i < array.length; i++) {
                    if (this.compareSelector(array[i].key) === compareKey) {
                        this.entryList.replace(array[i], entry);
                        array[i] = entry;
                        return;
                    }
                }
                array.push(entry);
            } else {
                this.buckets[hash] = [entry];
            }
            this.countField++;
            this.entryList.addLast(entry);
        },

        get: function (key) {
            var compareKey = this.compareSelector(key);
            var hash = computeHashCode(compareKey);
            if (!callHasOwnProperty(this.buckets, hash)) return undefined;

            var array = this.buckets[hash];
            for (let i = 0; i < array.length; i++) {
                const entry = array[i];
                if (this.compareSelector(entry.key) === compareKey) return entry.value;
            }
            return undefined;
        },

        set: function (key, value) {
            var compareKey = this.compareSelector(key);
            var hash = computeHashCode(compareKey);
            if (callHasOwnProperty(this.buckets, hash)) {
                const array = this.buckets[hash];
                for (let i = 0; i < array.length; i++) {
                    if (this.compareSelector(array[i].key) === compareKey) {
                        const newEntry = new HashEntry(key, value);
                        this.entryList.replace(array[i], newEntry);
                        array[i] = newEntry;
                        return true;
                    }
                }
            }
            return false;
        },

        contains: function (key) {
            var compareKey = this.compareSelector(key);
            var hash = computeHashCode(compareKey);
            if (!callHasOwnProperty(this.buckets, hash)) return false;

            var array = this.buckets[hash];
            for (let i = 0; i < array.length; i++) {
                if (this.compareSelector(array[i].key) === compareKey) return true;
            }
            return false;
        },

        clear: function () {
            this.countField = 0;
            this.buckets = {};
            this.entryList = new EntryList();
        },

        remove: function (key) {
            var compareKey = this.compareSelector(key);
            var hash = computeHashCode(compareKey);
            if (!callHasOwnProperty(this.buckets, hash)) return;

            var array = this.buckets[hash];
            for (let i = 0; i < array.length; i++) {
                if (this.compareSelector(array[i].key) === compareKey) {
                    this.entryList.remove(array[i]);
                    array.splice(i, 1);
                    if (array.length == 0) delete this.buckets[hash];
                    this.countField--;
                    return;
                }
            }
        },

        count: function () {
            return this.countField;
        },

        toEnumerable: function () {
            var self = this;
            return new Enumerable(function () {
                var currentEntry;

                return new IEnumerator(
                    function () { currentEntry = self.entryList.first; },
                    function () {
                        if (currentEntry != null) {
                            const result = { key: currentEntry.key, value: currentEntry.value };
                            currentEntry = currentEntry.next;
                            return this.yieldReturn(result);
                        }
                        return false;
                    },
                    Functions.Blank);
            });
        }
    };

    return Dictionary;
})();

// dictionary = Dictionary<TKey, TValue[]>
var Lookup = function (dictionary) {
    this.count = function () {
        return dictionary.count();
    };
    this.get = function (key) {
        return Enumerable.from(dictionary.get(key));
    };
    this.contains = function (key) {
        return dictionary.contains(key);
    };
    this.toEnumerable = function () {
        return dictionary.toEnumerable().select(function (kvp) {
            return new Grouping(kvp.key, kvp.value);
        });
    };
};

var Grouping = function (groupKey, elements) {
    this.key = function () {
        return groupKey;
    };
    ArrayEnumerable.call(this, elements);
};
Grouping.prototype = new ArrayEnumerable();

export default Enumerable;
